# coding=utf-8
"""
作者：董新强
创建时间：2020-3-12
描述：能力测试(假设曝光度为1的都被却掉了,因为这样的题没用处)
"""
from datetime import datetime

import numpy
from sqlalchemy import desc, func

from db_models.cat_answers import CatAnswers
from db_models.cat_questions import CatQuestions
from db_models.cat_test import CatTest
from db_models.cat_test_logs import CatTestLogs
from framework.db_session import DbSession
from framework.log_util import trycatch
from framework.msg import failure, ok
from framework.utilities import to_list
from services.cat_service.evaluator import Evaluator
from services.cat_service.question_bank import QuestionBank
from services.cat_service.selector import Selector
from services.cat_service.stoper import Stoper
from services.service_base import ServiceBase
from setting import MAX_TEST_ITEM_NUM


class CatManage(ServiceBase):
    def __init__(self, usr, ability_id, init_theta=0.5, max_exposure_rate=1, selector='difficult', pre_answers=None):
        super().__init__(usr)
        self.aid = ability_id
        self._selector = selector
        self._stop_num = MAX_TEST_ITEM_NUM

        self.init_theta = init_theta  # 初始能力值
        self.max_exposure_rate = max_exposure_rate  # 曝光度
        self._pre_answers = pre_answers

    @staticmethod
    def format(question_instance):
        """
        对返回给前端的数据进行格式化
        """
        if not question_instance:
            return {}
        return {
            'id': question_instance.id,
            'title': question_instance.title,
            'options': to_list(question_instance.options),
            'type': question_instance.type
        }

    @trycatch(failure(msg='答题失败'))
    def to_answer(self, qid, answer, is_test=False):
        """答题"""
        if not qid or answer is None:
            raise Exception('答题信息错误')

        with DbSession.create() as db:

            # 确定是否已经答过改题目
            test = db.query(CatTest).filter(CatTest.uid == self.uid).filter(CatTest.aid == self.aid).first()

            if not test:
                return failure(msg='测试不存在')

            if test.status:  # 测试结束
                return ok(data={'finished': 1, 'score': test.score})

            answer_ls, qid_ls, theta_ls = self.get_pre_answers(db, test_id=test.id)

            if qid in qid_ls:  # 已经答过的话,选择新的题目
                return self.start_test(db)

            # 获取答题的theta
            std_answer = db.query(CatQuestions.answer).filter(CatQuestions.id == qid).scalar()

            # 添加答案已经问题ID到列表里
            # 答案列表
            answer_ls.append(answer if is_test else std_answer == answer.lower())

            # 选择的问题ID的列表
            qid_ls.append(qid)

            # 选择题的参数的列表
            selected_items_params_ls = QuestionBank.get_item_params(self.aid, qid_ls)

            # theta的列表
            theta_ls.append(float(Evaluator.estimate(answer_ls, selected_items_params_ls)))  # 计算theta

            a = CatAnswers()
            a.qid = qid
            a.answer = answer_ls[-1]
            a.test_id = test.id
            a.theta = theta_ls[-1]
            db.add(a)

            if Stoper(theta_ls, selected_items_params_ls, answer_ls, max_item_num=self._stop_num).stop:
                return ok(data={'finished': 1, 'score': self._set_test_to_finished(answer_ls, db, test, theta_ls)})  # 测试结束

            # 选择新的题目
            result = self._select_new_item(db, qid_ls, theta_ls)
            if not result:  # 没有题目了,测试就结束了
                return ok(data={'finished': 1, 'score': self._set_test_to_finished(answer_ls, db, test, theta_ls)})  # 测试结束

            # 如果项目还没结束
            data = CatManage.format(result)
            data['answers'] = self._pre_answers
            data['msg'] = '回答正确' if answer_ls[-1] else '回答错误'
            return ok(data=data)

    def _set_test_to_finished(self, answer_ls, db, test, theta_ls):
        """
        设置测试为完成状态
        @param answer_ls:答案列表
        @param db: 数据库连接
        @param test: 测试
        @param theta_ls: 能力列表
        @return: 得分
        """
        # 设置测试已完成-----------------------
        score = CatManage._fina_theta(theta_ls, answer_ls, self.aid, db)
        score = max(min(int(score * 100), 100), 0)
        test.score = score
        test.status = True  # 测试完成
        test.end_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

        # 日志记录
        test_log = CatTestLogs()
        test_log.test_id = test.id
        test_log.aid = test.aid
        db.add(test_log)

        # ----------------------------------------------
        return score

    @staticmethod
    def _fina_theta(theta_ls, answer_ls, aid, db):
        """
        最终theta(得分)
        @param theta_ls: 能力列表
        @param answer_ls: 答案列表
        @param aid: 能力ID
        @param db: 数据库连接
        @return: 如果预测能力>0,才用计算出来的theta,但如果theta比最低难度系数还低,就返回 theta和最低难度系数 * 0.5 两者最大的得分
                 否则,预测能力为0分:按每正确一题的1分计算,得分等于=min[ sum(answer)/count(answer) , 最小难度系数*0.5 ]
                 这样,也可以保证预测的分数大于直接得零分的人是得分,也保证最终得分不为0
        """
        if not theta_ls:
            return 0

        min_b = db.query(func.min(CatQuestions.b)).filter(CatQuestions.aid == aid).scalar()  # 最低难度系数

        score = (numpy.mean(theta_ls[-4:]) + 3) / 6.0  # 归一化处理
        if score > 0:
            # 如果预测能力>0,才用计算出来的theta,但如果theta比最低难度系数还低,就返回最低难度系数,否则也太寒碜了吧
            return score if score > min_b else max(score, min_b * 0.5)  # 保证得分不低于0.5倍难度系数

        # 如果用户得分为零的情况,可能是他的得分真的为零,也可能是因为题目有问题,
        # 总之一句话,不应该让他的得分为零
        # 但他的得分应该不高于该套试卷最低的难度系数
        score = 0.0
        for a in answer_ls:
            if a:
                score += 1.0
        return max(0, min(score / len(answer_ls), min_b * 0.5))  # 保证得不高于 0.5倍难度系数

    def start_test(self, db=None):
        """开始测试"""

        def start(sess):
            """开始测试"""

            test = db.query(CatTest).filter(CatTest.uid == self.uid).filter(CatTest.aid == self.aid).first()
            if test and test.status:
                return ok(data={'finished': 1, 'score': test.score})  # 测试结束

            answer_ls, qid_ls, theta_ls = [], [], []
            if not test:  # 开始一个新的测试
                test = CatTest()
                test.aid = self.aid
                test.uid = self.uid
                test.status = False
                test.start_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                db.add(test)
                db.flush()
            else:  # 已经有测试记录了
                answer_ls, qid_ls, theta_ls = self.get_pre_answers(sess, test_id=test.id)

                # 检查是否可以停止测试
                if Stoper(theta_ls, QuestionBank.get_item_params(self.aid, qid_ls), answer_ls, max_item_num=self._stop_num).stop:
                    test.status = 1
                    return ok(data={'finished': 1, 'score': CatManage._fina_theta(theta_ls, answer_ls, self.aid, sess)})  # 测试结束

            # 检查是否答过题
            if not answer_ls:  # 如果还没答过题,选择一道题
                result = sess.query(CatQuestions).filter(CatQuestions.id == QuestionBank.get_qid(self.aid)).first()
                return ok(data=CatManage.format(result))

            result = self._select_new_item(db, qid_ls, theta_ls)
            if not result:  # 没有题目了,测试就结束了
                return ok(data={'finished': 1, 'score': self._set_test_to_finished(answer_ls, db, test, theta_ls)})

            # 如果测试还没结束
            return ok(data=CatManage.format(result))

        if db is not None:
            return start(db)
        with DbSession.create() as db:
            return start(db)

    def _select_new_item(self, db, qid_ls, theta_ls):
        if self._selector == 'difficult':
            qid = Selector.select_item_use_difficult(self.aid, self.init_theta, theta_ls, qid_ls)
        else:  # 默认为最大信息量
            qid = Selector.select_item_use_max_info(self.aid, self.init_theta, theta_ls, qid_ls, self.max_exposure_rate)

        if not qid:
            return None

        question = db.query(CatQuestions).filter(CatQuestions.id == qid).first()
        return question

    def get_pre_answers(self, db, test_id):
        if self._pre_answers:
            return self._pre_answers

        qid_ls, answer_ls, theta_ls = [], [], []
        query = db.query(CatAnswers.qid, CatAnswers.answer, CatAnswers.theta) \
            .filter(CatAnswers.test_id == test_id) \
            .order_by(desc(CatAnswers.id)) \
            .limit(self._stop_num)

        for qid, answer, theta in query:
            answer_ls.insert(0, answer)
            qid_ls.insert(0, qid)
            theta_ls.insert(0, theta)
        self._pre_answers = [answer_ls, qid_ls, theta_ls]
        return self._pre_answers
